package com.lyncs.ods.modules.txn.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.stream.CollectorUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.lyncs.ods.annotation.NeedLog;
import com.lyncs.ods.common.api.CommonPage;
import com.lyncs.ods.common.web.RequestHolder;
import com.lyncs.ods.constant.LyncsOdsConstant;
import com.lyncs.ods.modules.company.model.CompanyInfo;
import com.lyncs.ods.modules.company.service.CompanyInfoService;
import com.lyncs.ods.modules.txn.model.CommonTxnSubItem;
import com.lyncs.ods.modules.txn.model.TxnCommonInfo;
import com.lyncs.ods.modules.txn.model.TxnEditLog;
import com.lyncs.ods.modules.txn.mapper.TxnEditLogMapper;
import com.lyncs.ods.modules.txn.service.TxnEditLogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lyncs.ods.modules.user.model.CompanyUserRelation;
import com.lyncs.ods.modules.user.model.UserInfo;
import com.lyncs.ods.modules.user.service.CompanyUserRelationService;
import com.lyncs.ods.modules.user.service.UserInfoService;
import com.lyncs.ods.resp.TxnEditLogResp;
import com.lyncs.ods.utils.PageUtil;
import com.lyncs.ods.utils.TxnCommonUtils;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * <p>
 * ods编辑历史记录 服务实现类
 * </p>
 *
 * @author alex
 * @since 2022-04-09
 */
@Service
@Slf4j
public class TxnEditLogServiceImpl extends ServiceImpl<TxnEditLogMapper, TxnEditLog> implements TxnEditLogService {

    @Autowired
    private CompanyUserRelationService companyUserRelationService;

    @Override
    public <T extends TxnCommonInfo> void saveLog(T oldEntity, T newEntity, LyncsOdsConstant.TxnType type, Long refId, Long logId, Supplier<String> companyFunc, Supplier<String> userFunc) {
        LyncsOdsConstant.TxnEditType operateType = Objects.isNull(oldEntity) ? LyncsOdsConstant.TxnEditType.ADD : Objects.isNull(newEntity) ? LyncsOdsConstant.TxnEditType.DELETE : LyncsOdsConstant.TxnEditType.EDIT;

        String title = StringUtils.join(LyncsOdsConstant.Common.AT_SIGN, companyFunc.get(), LyncsOdsConstant.Common.BLANK, operateType.getDesc(), "了一笔", type.getDesc());
        Map<String, Object> oldContent = getModInfos(oldEntity);
        Map<String, Object> newContent = getModInfos(newEntity);
        String description = getDescription(oldContent, newContent);
        if (StringUtils.isEmpty(description)) {
            return;
        }
        TxnEditLog txnEditLog = new TxnEditLog()
                .setLogId(logId)
                .setType(type.getKey())
                .setRefId(refId)
                .setTitle(title)
                .setDescription(description)
                .setOperator(RequestHolder.getUserId())
                .setOldContent(oldContent.toString())
                .setNewContent(newContent.toString())
                .setOperateType(operateType.getKey())
                .setOperatorCompanyId(RequestHolder.getCompanyId())
                .setCreateTime(RequestHolder.getRequestDateTime());
        this.save(txnEditLog);
    }

    @Override
    public <T extends CommonTxnSubItem> void saveSubItemLog(List<T> insertSubItems, List<Long> deleteSubItems, List<Pair<T, T>> updateSubItems, LyncsOdsConstant.TxnType type, LyncsOdsConstant.TxnEditType editType, LyncsOdsConstant.TxnType subItemType, Long refId, Long logId, Supplier<String> companyFunc, Supplier<String> userFunc, Map<Long, String> nameMap) {
        List<TxnEditLog> txnEditLogs = new ArrayList<>();
        LyncsOdsConstant.TxnEditType operateType;
        //delete
        if (CollectionUtil.isNotEmpty(deleteSubItems)) {
            operateType = LyncsOdsConstant.TxnEditType.DELETE;
            String title = StringUtils.join(LyncsOdsConstant.Common.AT_SIGN, companyFunc.get(), LyncsOdsConstant.Common.BLANK, operateType.getDesc(), "了部分", subItemType.getDesc());
            String description = deleteSubItems.stream().map(nameMap::get).filter(Objects::nonNull).collect(Collectors.joining(LyncsOdsConstant.Common.BR));
            txnEditLogs.add(new TxnEditLog()
                    .setLogId(logId)
                    .setType(type.getKey())
                    .setRefId(refId)
                    .setTitle(title)
                    .setDescription(description)
                    .setOperator(RequestHolder.getUserId())
                    .setOldContent(JSONObject.toJSONString(deleteSubItems))
                    .setNewContent(null)
                    .setOperateType(editType.getKey())
                    .setOperatorCompanyId(RequestHolder.getCompanyId())
                    .setCreateTime(RequestHolder.getRequestDateTime()));
        }
        //insert
        if (CollectionUtil.isNotEmpty(insertSubItems)) {
            operateType = LyncsOdsConstant.TxnEditType.ADD;
            String title = StringUtils.join(LyncsOdsConstant.Common.AT_SIGN, companyFunc.get(), LyncsOdsConstant.Common.BLANK, operateType.getDesc(), "了部分", subItemType.getDesc());
            String description = insertSubItems.stream().map(i -> getDescription(null, getModInfos(i))).collect(Collectors.joining(LyncsOdsConstant.Common.BR));
            txnEditLogs.add(new TxnEditLog()
                    .setLogId(logId)
                    .setType(type.getKey())
                    .setRefId(refId)
                    .setTitle(title)
                    .setDescription(description)
                    .setOperator(RequestHolder.getUserId())
                    .setOldContent(null)
                    .setNewContent(JSONObject.toJSONString(insertSubItems))
                    .setOperateType(editType.getKey())
                    .setOperatorCompanyId(RequestHolder.getCompanyId())
                    .setCreateTime(RequestHolder.getRequestDateTime()));
        }
        //update
        if (CollectionUtil.isNotEmpty(updateSubItems)) {
            List<T> oldSubItems = updateSubItems.stream().map(Pair::getLeft).collect(Collectors.toList());
            List<T> newSubItems = updateSubItems.stream().map(Pair::getRight).collect(Collectors.toList());

            operateType = LyncsOdsConstant.TxnEditType.EDIT;
            String title = StringUtils.join(LyncsOdsConstant.Common.AT_SIGN, companyFunc.get(), LyncsOdsConstant.Common.BLANK, operateType.getDesc(), "了部分", subItemType.getDesc());
            String description = getDescription(oldSubItems, newSubItems, nameMap);
            if (StringUtils.isNotEmpty(description)) {
                txnEditLogs.add(new TxnEditLog()
                        .setLogId(logId)
                        .setType(type.getKey())
                        .setRefId(refId)
                        .setTitle(title)
                        .setDescription(description)
                        .setOperator(RequestHolder.getUserId())
                        .setOldContent(JSONObject.toJSONString(oldSubItems))
                        .setNewContent(JSONObject.toJSONString(newSubItems))
                        .setOperateType(editType.getKey())
                        .setOperatorCompanyId(RequestHolder.getCompanyId())
                        .setCreateTime(RequestHolder.getRequestDateTime()));
            }
        }
        this.saveBatch(txnEditLogs.stream().filter(i -> StringUtils.isNotBlank(i.getDescription())).collect(Collectors.toList()));
    }

    private <T> Map<String, Object> getModInfos(T entity) {
        Map<String, Object> info = new HashMap<>(8);
        if (Objects.isNull(entity)) {
            return info;
        }
        Field[] fields = entity.getClass().getDeclaredFields();
        if (ArrayUtil.isEmpty(fields)) {
            return info;
        }
        try {
            for (Field field : fields) {
                if (!field.canAccess(entity)) {
                    field.trySetAccessible();
                }
                Object fieldVal = field.get(entity);
                String annotationVal = null;
                boolean needLog = field.isAnnotationPresent(NeedLog.class);
                if (!needLog) {
                    log.info("No logging required :{}", field);
                    continue;
                }
                boolean annotationPresent = field.isAnnotationPresent(ApiModelProperty.class);
                if (annotationPresent) {
                    ApiModelProperty annotation = field.getAnnotation(ApiModelProperty.class);
                    // 获取注解值
                    annotationVal = annotation.value();
                }
                if (StringUtils.isEmpty(annotationVal)) {
                    log.info("get annotation value failed:{}", field);
                    continue;
                }
                info.put(annotationVal, fieldVal);
            }
        } catch (Exception e) {
            log.info("get annotation value failed,", e);
        }
        return info;
    }

    private <T> List<Map<String, Object>> getModInfos(List<T> list) {
        List<Map<String, Object>> arr = new ArrayList<>();
        if (CollectionUtil.isEmpty(list)) {
            return arr;
        }
        list.forEach(entity -> {
            Map<String, Object> info = getModInfos(entity);
            if (CollectionUtil.isNotEmpty(info)) {
                arr.add(info);
            }
        });
        return arr;
    }


    private String getDescription(Map<String, Object> oldContent, Map<String, Object> newContent) {
        StringBuilder sb = new StringBuilder();
        if (MapUtils.isNotEmpty(oldContent) && MapUtils.isNotEmpty(newContent)) {
            Map<String, MapDifference.ValueDifference<Object>> differenceMap = Maps.difference(oldContent, newContent).entriesDiffering();
            differenceMap.forEach((k, v) -> {
                sb.append(k)
                        .append(LyncsOdsConstant.Common.COLON)
                        .append(v.leftValue() == null ? "空" : v.leftValue())
                        .append("->")
                        .append(v.rightValue() == null ? "空" : v.rightValue())
                        .append(LyncsOdsConstant.Common.BR);
            });
        } else if (MapUtils.isNotEmpty(oldContent)) {
            oldContent.forEach((k, v) -> {
                if (Objects.nonNull(v)) {
                    sb.append(k)
                            .append(LyncsOdsConstant.Common.COLON)
                            .append(v)
                            .append(LyncsOdsConstant.Common.BR);
                }
            });
        } else {
            newContent.forEach((k, v) -> {
                if (Objects.nonNull(v)) {
                    sb.append(k)
                            .append(LyncsOdsConstant.Common.COLON)
                            .append(v)
                            .append(LyncsOdsConstant.Common.BR);
                }
            });
        }
        return sb.toString();
    }

    private <T extends CommonTxnSubItem> String getDescription(List<T> oldContent, List<T> newContent, Map<Long, String> nameMap) {
        if (CollectionUtils.isEmpty(oldContent) || CollectionUtils.isEmpty(newContent) || oldContent.size() != newContent.size()) {
            log.error("invalid data,old:{},new:{}", oldContent, newContent);
            return "";
        }
        StringBuilder sb = new StringBuilder();
        Map<Long, T> oldContentMap = oldContent.stream().collect(Collectors.toMap(CommonTxnSubItem::getId, o -> o));
        Map<Long, T> newContentMap = newContent.stream().collect(Collectors.toMap(CommonTxnSubItem::getId, o -> o));
        oldContentMap.forEach((k, v) -> {
            if (!newContentMap.containsKey(k)) {
                return;
            }
            T newInfo = newContentMap.get(k);
            Map<String, Object> oldModMap = this.getModInfos(v);
            Map<String, Object> newModMap = this.getModInfos(newInfo);

            //sku/fee name
            sb.append("<b>").append(nameMap.get(v.getSubItemId())).append("</b>").append("<br/>").append(getDescription(oldModMap, newModMap));
        });
        return sb.toString();
    }

    @Override
    public CommonPage<TxnEditLogResp> getEditLog(Integer type, Long refId, Integer page, Integer pageSize) {
        //SELECT * FROM txn_edit_log WHERE log_id IN (SELECT tmp.* FROM(SELECT DISTINCT(log_id) FROM txn_edit_log GROUP BY log_id ORDER BY create_time DESC LIMIT 5) AS tmp)
        List<TxnEditLog> editLogList = this.lambdaQuery()
                .eq(TxnEditLog::getType, type)
                .eq(TxnEditLog::getRefId, refId)
                .orderByDesc(TxnEditLog::getId)
                .list();
        long count = editLogList.stream().map(TxnEditLog::getLogId).distinct().count();
        CommonPage<TxnEditLogResp> commonPage = new CommonPage<TxnEditLogResp>().setPage(page).setPageSize(pageSize).setTotal(count);
        if (count == 0) {
            return commonPage;
        }
        //物理分页
        Integer offset = PageUtil.getOffset(page, pageSize);
        List<Long> logIds = editLogList.stream().map(TxnEditLog::getLogId).distinct().skip(offset).limit(pageSize).collect(Collectors.toList());
        List<TxnEditLog> txnEditLogs = editLogList.stream().filter(i -> logIds.contains(i.getLogId())).collect(Collectors.toList());
        LambdaQueryChainWrapper<CompanyUserRelation> lambdaQueryWrapper = companyUserRelationService.lambdaQuery();
        for (TxnEditLog log : txnEditLogs) {
            lambdaQueryWrapper.or(wrapper -> wrapper.eq(CompanyUserRelation::getCompanyId, log.getOperatorCompanyId()).eq(CompanyUserRelation::getUserId, log.getOperator()));
        }
        List<CompanyUserRelation> companyUserRelations = lambdaQueryWrapper.list();
        Map<String, String> companyUserNamMap = companyUserRelations.stream().collect(Collectors.toMap(TxnCommonUtils::buildCompanyUserKey, CompanyUserRelation::getNickname, (o1, o2) -> o1));
        LinkedHashMap<Long, List<TxnEditLog>> logMap = txnEditLogs.stream().collect(Collectors.groupingBy(TxnEditLog::getLogId, LinkedHashMap::new, Collectors.toList()));
        return commonPage.setList(logMap.entrySet().stream().map(entry -> {
            List<TxnEditLog> editLogs = entry.getValue().stream().sorted(Comparator.comparing(TxnEditLog::getId)).collect(Collectors.toList());
            TxnEditLog first = editLogs.get(0);
            return new TxnEditLogResp()
                    .setLogId(entry.getKey())
                    .setOperator(companyUserNamMap.get(TxnCommonUtils.buildLong2StringKey(first.getOperatorCompanyId(), first.getOperator())))
                    .setTitle(first.getTitle())
                    .setOperatorTime(first.getCreateTime().toLocalDate())
                    .setType(first.getOperateType())
                    .setDetails(editLogs.stream().map(TxnEditLog::getDescription).collect(Collectors.toList()));
        }).collect(Collectors.toList()));
    }
}
